/*! Quilt: A JavaScript preprocessor written in JavaScript
* (c) 2010 Kit Goncharov (http://kitgoncharov.github.com)
*
* Quilt is freely distributable under the terms of the MIT License.
* Details and documentation: http://github.com/kitgoncharov/quilt.
*
* ---------------------------------------------------------------------------*/

/*jslint white: true, forin: true, onevar: true, maxlen: 80, indent: 2 */
(function (global) {
  // Aliases to several common Java packages and classes.
  var java = global.Packages && global.Packages.java, lang = java && java.lang,
  System = lang && lang.System, io = java && java.io, File = io && io.File,
  // Convenience aliases and the Quilt namespace.
  nil = null, Line, Script, Concatenation, Quilt = global.Quilt = {
    // Restores the original value of the global "Quilt" variable.
    "noConflict": (function (original) {
      // Capture the variable's previous value.
      return function noConflict() {
        /* "noConflict" can only be used once because there's no way to
        * determine which variable Quilt has been reassigned to.
        */
        delete Quilt.noConflict;
        // Restore the previous value.
        global.Quilt = original;
        // Return a reference to the Quilt object.
        return Quilt;
      };
    }(global.Quilt)),
    // The current version of Quilt.
    "VERSION": "0.6.0"
  }, getClass = Object.prototype.toString;
  
  Line = Quilt.Line = (function () {
    var fn;
    
    function Line(script, line, lineNumber) {
      this.script = script;
      this.line = String(line);
      this.lineNumber = lineNumber;
    }
    fn = Line.prototype;
    
    // Parses "require" directives.
    fn.require = function require() {
      var directive, argument, script;
      if (!File) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      // Matches directives: //= require "..." or //= require <...>
      directive = /^\s*\/\/=\s+require\s+("(?:.+?)"|<(?:.+?)>)\s*$/;
      script = nil;
      if ((directive = directive.exec(this.line))) {
        // Strip the delimiters and add the ".js" file extension.
        argument = directive[1].slice(1, -1).replace(/\.js$/, "") + ".js";
        // Create either a relative or absolute path to the file.
        script = directive[1].charAt(0) === "<" ? argument : String((new File(
          this.script.path.getParent(), argument)).getCanonicalPath());
      }
      return script;
    };
    
    // Parses "provide" directives.
    fn.provide = function provide() {
      var directive, path;
      if (!File) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      // Matches directives: //= provide "..."
      directive = /^=\s+provide\s+"(.+?)"\s*$/;
      path = nil;
      if ((directive = directive.exec(this.line))) {
        // Strip the delimiters and create the file's path.
        path = String((new File(this.script.path.getParent(),
          directive[1].slice(1, -1))).getCanonicalPath());
      }
      return path;
    };
    
    fn.inspect = function inspect() {
      return this.script.path.getPath() + ":" + this.lineNumber;
    };
    
    fn.toString = function toString() {
      var constants = this.script.concatenation;
      // Internal method for looking up parameterized constants.
      function interpolate(match, constant) {
        constant = constant.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
        // Ignore unspecified constants.
        return constant in constants ? constants[constant] : match;
      }
      // Interpolate constants and strip trailing whitespace.
      return this.line.replace(/<%=(.+?)%>/, interpolate).replace(
        /\s\s*$/, "");
    };
    
    // Expose the constructor.
    return Line;
  }());
  
  Script = Quilt.Script = (function () {
    // Convenience aliases to Java classes.
    var fn, BigInteger = java && java.math && java.math.BigInteger,
    MessageDigest = java && java.security && java.security.MessageDigest,
    JavaString = lang && lang.String, BufferedReader = io && io.BufferedReader,
    FileReader = io && io.FileReader;
    
    function Script(concatenation, path) {
      if (!File) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      // Resolve the script's canonical path.
      path = this.path = (path && path instanceof File ? path :
        new File(String(path))).getCanonicalPath();
      // Make sure that the specified script exists.
      if (!path.isFile()) {
        throw new Error("The script " + path.getPath() + " was not found.");
      }
      this.concatenation = concatenation;
      this.lines = [];
    }
    fn = Script.prototype;
    
    fn.readLines = function readLines() {
      var stream, line, lineno;
      if (!BufferedReader || !FileReader) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      stream = new BufferedReader(new FileReader(this.path));
      lineno = 1;
      while ((line = stream.readLine()) !== nil) {
        this.lines.push(new Quilt.Line(this, line, lineno++));
      }
      return this;
    };
    
    // Returns the script's modification time.
    fn.lastModified = function lastModified() {
      return new Date(this.path.lastModified());
    };
    
    fn.forEach = function forEach(iterator, context) {
      var lines = this.lines, length = lines.length;
      if (getClass.call(iterator) !== "[object Function]") {
        throw new TypeError();
      }
      while (length--) {
        iterator.call(context, lines[length], length, lines);
      }
      return this;
    };
    
    // Calculates the script's MD5 checksum.
    fn.checksum = function checksum() {
      if (!BigInteger || !MessageDigest || !JavaString) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      return String((new BigInteger(1, MessageDigest.getInstance("MD5").digest(
        (new JavaString(this.toString())).getBytes("UTF-8")))).toString(16));
    };
    
    fn.toString = function toString(separator) {
      return this.lines.join(typeof separator === "string" ? separator : "\n");
    };
    
    // Expose the constructor.
    return Script;
  }());
  
  Concatenation = Quilt.Concatenation = (function () {
    var fn, BufferedWriter = io && io.BufferedWriter,
    FileWriter = io && io.FileWriter;
    
    function Concatenation(options) {
      if (!(this instanceof Concatenation)) {
        return new Concatenation(options);
      }
      var locations, length, location;
      options = Object(options);
      // The Quilt working directory (defaults to the current directory).
      this.path = options.path || ".";
      // An object containing optional string constants for interpolation.
      this.constants = options.constants || {};
      /* An ordered array of locations (either absolute or relative to the
      * working directory) in which Quilt should search for scripts and any
      * specified dependencies.
      */
      this.locations = [];
      if ((locations = options.locations)) {
        if (!File) {
          throw new Error("This operation requires Mozilla Rhino.");
        }
        length = locations.length;
        while (length--) {
          location = new File(locations[length]);
          /* Absolute search locations are left as-is; relative locations are
          * canonicalized and resolved against the Quilt working directory.
          */
          this.locations[length] = String(location.isAbsolute() ?
            location.getPath() : (new File(this.path,
              location.getPath())).getCanonicalPath());
        }
      }
      // The concatenation's, lines, modification time, and MD5 checksum.
      this.lines = [];
      this.timestamp = 0;
      this.checksum = null;
      // Included scripts.
      this.scripts = [];
    }
    fn = Concatenation.prototype;
    
    fn.find = function find(path, isDirectory) {
      var index, location, match;
      if (!File) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      if (!(path instanceof File)) {
        path = new File(path);
      }
      if (path.isAbsolute()) {
        // Resolve the canonical path of the file or directory.
        return path["is" + (isDirectory === true ? "Directory" : "File")]() ?
          String(path.getCanonicalPath()) : nil;
      }
      // Search the Quilt load path for the requested file or directory.
      index = 0;
      while ((location = this.locations[index++])) {
        if ((match = this.find(new File(location, path.getPath()),
          isDirectory))) {
          // Return the first match.
          return match;
        }
      }
      return nil;
    };
    
    // Adds a script to the concatenation.
    fn.addScript = function addScript(script) {
      var length, timestamp;
      script = new Script(script);
      // Verify that the script hasn't already been included.
      length = this.scripts.length;
      while (length--) {
        // Compare the checksums of the two scripts.
        if (this.scripts[length].checksum() === script.checksum()) {
          return this;
        }
      }
      // Add the script to the concatenation.
      this.scripts.push(script);
      // Update the concatenation's modification time accordingly.
      timestamp = script.lastModified();
      if (timestamp > this.timestamp) {
        this.timestamp = timestamp;
      }
      return this;
    };
    
    fn.concatenate = function concatenate() {
      // ...
    };
    
    fn.save = function save(destination) {
      var directory, stream;
      if (!File || !BufferedWriter || !FileWriter) {
        throw new Error("This operation requires Mozilla Rhino.");
      }
      if (!(destination && destination instanceof File)) {
        destination = new File(String(destination));
      }
      // Resolve relative paths against the Quilt working directory.
      destination = (destination.isAbsolute() ? destination : new File(
        this.path, destination.getPath())).getCanonicalFile();
      // Recursively create all destination directories if they don't exist.
      directory = destination.getParent();
      if (!(directory && directory.isDirectory() && directory.mkdirs())) {
        throw new Error("The destination directory `" + directory.getPath() +
          "` could not be created.");
      }
      stream = new BufferedWriter(new FileWriter(destination));
      stream.write(this.toString());
      stream.close();
      // Set the file's modification time.
      destination.setLastModified(+(this.timestamp || new Date()));
      return this;
    };
    
    fn.checksum = Script.prototype.checksum;
    fn.toString = Script.prototype.toString;
    
    // Expose the constructor.
    return Concatenation;
  }());
}(this));